package org.road0329.dlt698.sessionlayer.hdlc;

import org.road0329.dlt698.DataUtils;
import org.road0329.dlt698.ReverseByteArrayInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class HdlcFrame {
    private static Logger logger = LoggerFactory.getLogger(HdlcFrame.class);
    private static final int FLAGBEG = 0x68;
    private static final int FLAGEND = 0x16;
    private int frameType = 0;
    private int controlField = 0;
    private byte[] informationField = null;
    private boolean segmented = false;
    private HdlcAddressPair addressPair = null;
    private int segframexh = 0;
    private int segframetype = 0;

    public HdlcFrame() {
    }

    public void setAddpairFrametype(HdlcAddressPair addressPair, int frameType) {
        this.addressPair = addressPair;
        this.frameType = frameType;
    }

    public void setAllParam(int frameType, byte[] informationField, boolean segmented,
                            int controlField, HdlcAddressPair addressPair, int segframexh,
                            int segframetype) {
        this.frameType = frameType;
        this.informationField = informationField;
        this.segmented = segmented;
        this.controlField = controlField;
        this.addressPair = addressPair;
        this.segframexh = segframexh;
        this.segframetype = segframetype;
    }

    public static HdlcFrame newCliReqFrame(HdlcAddressPair addressPair, byte[] information, boolean segmented) {
        HdlcFrame hdlcFrame = new HdlcFrame();
        hdlcFrame.setAddpairFrametype(addressPair, FrameType.CLI_REQ.getValue());
        hdlcFrame.informationField = information;
        hdlcFrame.segmented = segmented;
        hdlcFrame.controlField = CmdType.CMD_USERDATA.getValue();
        return hdlcFrame;
    }

    public HdlcFrame decode(ReverseByteArrayInputStream input) throws Exception {
        FcsCalc fcsCalc = new FcsCalc();
        int segframexh = 0;
        int segframetype = 0;
        int codePosition = 0;
        int frameFormatH = input.read();
        if ((frameFormatH & 0xFF) != FLAGBEG) {
            throw new Exception("Illegal frameformat");
        }
        int lenframe = (input.read() & 0xff);
        lenframe += (input.read() << 8) & 0xff00;
        ByteBuffer code = ByteBuffer.allocate(lenframe);
        code.order(ByteOrder.LITTLE_ENDIAN);
        code.put((byte) (lenframe & 0xff));
        code.put((byte) ((lenframe & 0xff00) >> 8));
        int controlField = input.read();
        code.put((byte) (controlField & 0xff));
        codePosition += 3;
        int cmdType = cmdTypeFor(controlField & 0xFF);
        int frameType = frameTypeFor(controlField & 0xFF);
        boolean segmented = (0x20 & controlField) == 0x20;
        HdlcAddress srvaddr = decodeAddress(input);
        logger.debug("server addr:{}", srvaddr);
        int cliaddr = input.read();
        logger.debug("client addr:{}", cliaddr);
        int tempbyte = input.read();
        logger.debug("tmp:{}", tempbyte);
        tempbyte = input.read();
        logger.debug("tmp:{}", tempbyte);

//        fcsCalc.validateCurrentFcsValue();
        byte[] informationField;
        int infoLength = lenframe - 8 - srvaddr.getByteLength();
        logger.debug("info length:{}", infoLength);
        if (infoLength > 0) {
            if (segmented) {
                infoLength = infoLength - 2;
                int frameinfo = input.read();
                segframexh = frameinfo & 0x3FFF;
                segframetype = (frameinfo >> 14) & 0x03;
                ByteBuffer codetemp = ByteBuffer.allocate(1000);
                codetemp.put((byte) (frameinfo));
                fcsCalc.update(codetemp.array(), 1);
            }
            informationField = new byte[infoLength];
            for (int i = 0; i < infoLength; i++) {
                int data = input.read();
                fcsCalc.updates(data);
                informationField[i] = (byte) data;
            }
            tempbyte = input.read();
            logger.debug("tmp:{}", tempbyte);
            fcsCalc.updates(tempbyte);
            tempbyte = input.read();
            logger.debug("tmp:{}", tempbyte);
            fcsCalc.updates(tempbyte);
//            fcsCalc.validateCurrentFcsValue();
        } else {
            informationField = new byte[0];
            tempbyte = input.read();
            fcsCalc.updates(tempbyte);
            tempbyte = input.read();
            fcsCalc.updates(tempbyte);
//            fcsCalc.validateCurrentFcsValue();
        }
        tempbyte = input.read();
        logger.debug("tmp:{}", tempbyte);
        HdlcAddressPair addressPair = new HdlcAddressPair(srvaddr, ByteBuffer.wrap(DataUtils.hexToBytes(DataUtils.intToHexString(cliaddr, 1))));
        HdlcFrame hdlcFrame = new HdlcFrame();
        hdlcFrame.setAllParam(frameType, informationField, segmented, cmdType,
                addressPair, segframexh, segframetype);
        return hdlcFrame;
    }

    public static HdlcAddress decodeAddress(ReverseByteArrayInputStream input) {
        int currentByte = input.read();
        int length = (currentByte & 0x0f) + 1;
        ByteBuffer data = ByteBuffer.allocate(length + 1);
        data.put((byte) currentByte);
        int i = 0;
        while (i < length) {
            currentByte = input.read();
            data.put((byte) currentByte);
            i++;
        }
        return HdlcAddress.decode(data);
    }

    public byte[] encode() throws Exception {
        ByteBuffer data = encodeWithoutFlags();
        byte[] temp = new byte[data.capacity() + 2];
        temp[0]= ((byte) FLAGBEG);
        System.arraycopy(data.array(), 0, temp, 1, data.capacity());
        temp[data.capacity() + 1] = (byte) FLAGEND;
        return temp;
    }

    public ByteBuffer encodeWithoutFlags() throws Exception {
        if (this.informationField != null) {
            int length = 8 + this.getSrvAddress().getByteLength();
            if (this.containsInformation()) {
                length += this.informationField.length;
            }
            if (this.isSegmented()) {
                length += 2;
            }
            logger.debug("length:{}", length);
            int codePosition = 0;
            ByteBuffer code = ByteBuffer.allocate(length);
            code.put((byte) (length & 0x00ff));
            code.put((byte) ((length & 0xff00) >> 8));
            code.put((byte) ((this.controlField & 0x03) | this.frameType | (this.isSegmented() ? 0x20 : 0x0)));
            codePosition += 3;
            ByteBuffer srvEncode = this.getSrvAddress().encode();
            srvEncode.rewind();
            code.put(srvEncode);
            codePosition += srvEncode.capacity();
            ByteBuffer cliEncode = this.getCliAddress();
            cliEncode.rewind();
            code.put(cliEncode);
            codePosition += cliEncode.capacity();
            FcsCalc fcsCalc = new FcsCalc();
            fcsCalc.update(code.array(), codePosition);
            byte[] hcsEncode = fcsCalc.fcsValueInBytes();
            code.put(hcsEncode);
            codePosition += hcsEncode.length;

            fcsCalc.update(fcsCalc.fcsValueInBytes(), fcsCalc.fcsValueInBytes().length);
            logger.debug("hcs is update");
            if (this.isSegmented()) {
                int segframeinfo = (this.segframexh & 0x3FFFF) | ((this.segframetype & 0x03) << 14);
                code.put((byte) (segframeinfo & 0x00ff));
                code.put((byte) ((segframeinfo & 0xff00) >> 8));
                codePosition += 2;
                byte[] segbyte = new byte[2];
                code.position(codePosition);
                code.get(segbyte);
                fcsCalc.update(segbyte, 2);
            }
            if (this.containsInformation()) {
                code.put(this.informationField);
                fcsCalc.update(this.informationField, this.informationField.length);
            }
            byte[] fcs = fcsCalc.fcsValueInBytes();
            code.put(fcs);
            return code;
        } else {
            throw new Exception("apdu data is null");
        }
    }

    public HdlcAddress getSrvAddress() throws Exception {
        if (this.addressPair != null) {
            return this.addressPair.getSrvAddr();
        } else {
            throw new Exception("addressPair is null");
        }
    }

    public ByteBuffer getCliAddress() throws Exception {
        if (this.addressPair != null) {
            return this.addressPair.getCliAddr();
        } else {
            throw new Exception("addressPair is null");
        }
    }

    public HdlcAddressPair getAddressPair() throws Exception {
        if (this.addressPair != null) {
            return this.addressPair;
        } else {
            throw new Exception("addressPair is null");
        }
    }

    public int getFrameType() {
        return this.frameType;
    }

    public byte[] getInformationField() throws Exception {
        if (this.informationField != null) {
            return this.informationField;
        } else {
            throw new Exception("apdu data is null");
        }
    }

    public boolean isSegmented() {
        return this.segmented;
    }

    public boolean containsInformation() {
        return this.informationField != null;
    }

    public void setSegFrameType(int segframetype2) {
        this.segframetype = segframetype2;
    }

    public void setSegFrameXh(int segframexh2) {
        this.segframexh = segframexh2;
    }

    public int getSegFrameType() {
        return this.segframetype;
    }

    public int getSegFrameXh() {
        return this.segframexh;
    }

    public enum CmdType {
        CMD_RESERVE0(0, 7),
        CMD_LINK(1, 7),
        CMD_RESERVE2(2, 7),
        CMD_USERDATA(3, 7),
        CMD_RESERVE4(4, 7),
        CMD_RESERVE5(5, 7),
        CMD_RESERVE6(6, 7),
        CMD_RESERVE7(7, 7);

        private final int value;
        private final int mask;

        CmdType(int value, int mask) {
            this.value = value;
            this.mask = mask;
        }

        public int getValue() {
            return value;
        }

        public int getMask() {
            return mask;
        }
    }

    public enum FrameType {
        CLI_RES(0x00, 0xc0),
        CLI_REQ(0x40, 0xc0),
        SRV_REQ(0x80, 0xc0),
        SRV_RES(0xc0, 0xc0),
        ERR_INVALID_TYPE(0xFF, 0xFF);

        private final int value;
        private final int mask;

        FrameType(int value, int mask) {
            this.value = value;
            this.mask = mask;
        }

        public int getValue() {
            return value;
        }

        public int getMask() {
            return mask;
        }
    }

    public static int cmdTypeFor(int controlByte) {
        for (CmdType cmdType : CmdType.values()) {
            if ((cmdType.getMask() & controlByte) == cmdType.getValue()) {
                return cmdType.getValue();
            }
        }
        return CmdType.CMD_USERDATA.getValue();
    }

    public static int frameTypeFor(int controlByte) {
        for (FrameType frameType : FrameType.values()) {
            if ((frameType.getMask() & controlByte) == frameType.getValue()) {
                return frameType.getValue();
            }
        }
        return FrameType.ERR_INVALID_TYPE.getValue();
    }


}



